﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Velocity4Net.Runtime.Parse.AST;
using Velocity4Net.Ctx;
using Velocity4Net.Runtime.Parse;
using Velocity4Net.Errors;
using System.IO;
using System.Collections;
using Velocity4Net.Errors;
using Velocity4Net.Util.Introspection;
using Velocity4Net.Runtime.Logging;

namespace Velocity4Net.Runtime.Directives
{
    /**
     * This class acts as a proxy for potential macros.  When the AST is built
     * this class is inserted as a placeholder for the macro (whether or not
     * the macro is actually defined).  At render time we check whether there is
     * a implementation for the macro call. If an implementation cannot be
     * found the literal text is rendered.
     * @since 1.6
     */
    public class RuntimeMacro : Directive
    {
        /**
         * Name of the macro
         */
        private String macroName;

        /**
         * Literal text of the macro
         */
        private String literal = null;

        /**
         * Node of the macro call
         */
        private INode node = null;

        /**
         * Indicates if we are running in strict reference mode.
         */
        protected bool strictRef = false;

        /**
         * badArgsErrorMsg will be non null if the arguments to this macro
         * are deamed bad at init time, see the init method.  If his is non null, then this macro 
         * cannot be rendered, and if there is an attempt to render we throw an exception
         * with this as the message.
         */
        private String badArgsErrorMsg = null;

        /**
         * Create a RuntimeMacro instance. Macro name and source
         * template stored for later use.
         *
         * @param macroName name of the macro
         */
        public RuntimeMacro(String macroName)
        {
            if (macroName == null)
            {
                throw new ArgumentNullException("Null arguments");
            }

            this.macroName = macroName;
        }

        /**
         * Return name of this Velocimacro.
         *
         * @return The name of this Velocimacro.
         */
        public override String Name
        {
            get
            {
                return macroName;
            }
            set { }
        }

        /**
         * Override to always return "macro".  We don't want to use
         * the macro name here, since when writing VTL that uses the
         * scope, we are within a #macro call.  The macro name will instead
         * be used as the scope name when defining the body of a BlockMacro.
         */
        public String getScopeName()
        {
            return "macro";
        }

        /**
         * Velocimacros are always LINE
         * type directives.
         *
         * @return The type of this directive.
         */
        public override int Type
        {
            get
            {
                return 2;
            }
        }


        /**
         * Intialize the Runtime macro. At the init time no implementation so we
         * just save the values to use at the render time.
         *
         * @param rs runtime services
         * @param context InternalContextAdapter
         * @param node node containing the macro call
         */
        public override void Init(RuntimeServices rs, InternalContextAdapter context,
                         INode node)
        {
            base.Init(rs, context, node);
            rsvc = rs;
            this.node = node;

            /**
             * Apply strictRef setting only if this really looks like a macro,
             * so strict mode doesn't balk at things like #E0E0E0 in a template.
             * compare with ")" is a simple #foo() style macro, comparing to
             * "#end" is a block style macro. We use starts with because the token
             * may end with '\n'
             */
            Token t = node.LastToken;
            if (t.image.StartsWith(")") || t.image.StartsWith("#end"))
            {
                strictRef = rsvc.GetBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
            }

            // Validate that none of the arguments are plain words, (VELOCITY-614)
            // they should be string literals, references, inline maps, or inline lists
            for (int n = 0; n < node.jjtGetNumChildren(); n++)
            {
                INode child = node.jjtGetChild(n);
                if (child.NodeType == ParserTreeConstants.JJTWORD)
                {
                    badArgsErrorMsg = "Invalid arg '" + child.FirstToken.image
                    + "' in macro #" + macroName + " at " + Log.formatFileString(child);

                    if (strictRef)  // If strict, throw now
                    {
                        /* indicate col/line assuming it starts at 0
                         * this will be corrected one call up  */
                        throw new TemplateInitException(badArgsErrorMsg,
                            context.CurrentTemplateName, 0, 0);
                    }
                }
            }
        }

        /**
         * It is probably quite rare that we need to render the macro literal
         * so do it only on-demand and then cache the value. This tactic helps to
         * reduce memory usage a bit.
         */
        private String getLiteral()
        {
            if (literal == null)
            {
                StringBuilder buffer = new StringBuilder();
                Token t = node.FirstToken;

                while (t != null && t != node.LastToken)
                {
                    buffer.Append(t.image);
                    t = t.next;
                }

                if (t != null)
                {
                    buffer.Append(t.image);
                }

                literal = buffer.ToString();
            }
            return literal;
        }


        /**
         * Velocimacro implementation is not known at the init time. So look for
         * a implementation in the macro libaries and if finds one renders it. The
         * actual rendering is delegated to the VelocimacroProxy object. When
         * looking for a macro we first loot at the template with has the
         * macro call then we look at the macro lbraries in the order they appear
         * in the list. If a macro has many definitions above look up will
         * determine the precedence.
         *
         * @param context
         * @param writer
         * @param node
         * @return true if the rendering is successful
         * @throws IOException
         * @throws ResourceNotFoundException
         * @throws ParseErrorException
         * @throws MethodInvocationException
         */
        public override bool Render(InternalContextAdapter context, TextWriter writer,
                              INode node)
        {
            return render(context, writer, node, null);
        }

        /**
         * This method is used with BlockMacro when we want to render a macro with a body AST.
         *
         * @param context
         * @param writer
         * @param node
         * @param body AST block that was enclosed in the macro body.
         * @return true if the rendering is successful
         * @throws IOException
         * @throws ResourceNotFoundException
         * @throws ParseErrorException
         * @throws MethodInvocationException
         */
        public bool render(InternalContextAdapter context, TextWriter writer,
                              INode node, Renderable body)
        {
            VelocimacroProxy vmProxy = null;
            String renderingTemplate = context.CurrentTemplateName;

            /**
             * first look in the source template
             */
            Object o = rsvc.GetVelocimacro(macroName, TemplateName, renderingTemplate);

            if (o != null)
            {
                // getVelocimacro can only return a VelocimacroProxy so we don't need the
                // costly instanceof check
                vmProxy = (VelocimacroProxy)o;
            }

            /**
             * if not found, look in the macro libraries.
             */
            if (vmProxy == null)
            {
                IList macroLibraries = context.MacroLibraries;
                if (macroLibraries != null)
                {
                    for (int i = macroLibraries.Count - 1; i >= 0; i--)
                    {
                        o = rsvc.GetVelocimacro(macroName,
                                (String)macroLibraries[i], renderingTemplate);

                        // get the first matching macro
                        if (o != null)
                        {
                            vmProxy = (VelocimacroProxy)o;
                            break;
                        }
                    }
                }
            }

            if (vmProxy != null)
            {
                try
                {
                    // mainly check the number of arguments
                    vmProxy.checkArgs(context, node, body != null);
                }
                catch (TemplateInitException die)
                {
                    throw new ParseErrorException(die.Message + " at "
                        + Log.formatFileString(node), new Info(node));
                }

                if (badArgsErrorMsg != null)
                {
                    throw new TemplateInitException(badArgsErrorMsg,
                      context.CurrentTemplateName, node.Column, node.Line);
                }

                try
                {
                    preRender(context);
                    return vmProxy.render(context, writer, node, body);
                }
                catch (StopCommand stop)
                {
                    if (!stop.isFor(this))
                    {
                        throw stop;
                    }
                    return true;
                }
                catch (ApplicationException e)
                {
                    /**
                     * We catch, the exception here so that we can record in
                     * the logs the template and line number of the macro call
                     * which generate the exception.  This information is
                     * especially important for multiple macro call levels.
                     * this is also true for the following catch blocks.
                     */
                    rsvc.getLog().error("Exception in macro #" + macroName + " called at " +
                      Log.formatFileString(node));
                    throw e;
                }
                catch (IOException e)
                {
                    rsvc.getLog().error("Exception in macro #" + macroName + " called at " +
                      Log.formatFileString(node));
                    throw e;
                }
                finally
                {
                    postRender(context);
                }
            }
            else if (strictRef)
            {
                throw new VelocityException("Macro '#" + macroName + "' is not defined at "
                    + Log.formatFileString(node));
            }

            /**
             * If we cannot find an implementation write the literal text
             */
            writer.Write(getLiteral());
            return true;
        }
    }
}
